package ua.kpi.ipsa.expert.api.algorithm;

import java.lang.reflect.*;
import java.util.*;

import ua.kpi.ipsa.expert.api.db.*;

/**
 * 
 * @author doctorpro
 * 
 *         NOT IMPLEMENTED YET
 */
public class AlgoRunManager {

	// map: codePortionTupleId |-> (map: algorithmId |-> algoRunData)
	private final Map<DoubleInt, InnerAlgoRunData> algoRunData = new HashMap<DoubleInt, InnerAlgoRunData>();
	private final DAO dao;
	private final AlgoLoaf algoLoaf;
	private final CodePresentationManager presentationManager;

	/**
	 * 
	 * @param dao
	 */
	public AlgoRunManager(DAO dao, CodePresentationManager presentationManager) {
		this.presentationManager = presentationManager;
		this.dao = dao;
		algoLoaf = new AlgoLoaf(dao, presentationManager);
	}

	/**
	 * 
	 * @param algorithmId
	 * @param codePortionId
	 */
	public synchronized void stopAlgorithm(Integer algorithmId,
			Integer codePortionId) {
		algoRunData.remove(new DoubleInt(codePortionId.intValue(), algorithmId
				.intValue())).thread.stop();
	}

	/**
	 * Method called from net subsystem directly. Puts algorithm on the
	 * conveyer.
	 * 
	 * @param algorithmId
	 * @param codePortionId
	 */
	public synchronized void addAlgorithm(final int algorithmId,
			final int codePortionId) throws DataAccessException {

		// check if the algorithm is working at the moment
		InnerAlgoRunData arData = algoRunData.get(new DoubleInt(codePortionId,
				algorithmId));

		if (arData == null) {

			try {

				// get algorithm instance
				final Algorithm algo = algoLoaf.getAlgorithm(algorithmId);

				// Check if code presentations are ready for the algorithm
				if (presentationManager.getAlgorithmReadiness(algo) < codePortionId)
					throw new DataAccessException("Algorithm is not ready yet.");

				// add into dow
				dao.setAssignedAlgorithm(new AssignedAlgorithmTuple(
						codePortionId, algorithmId));

				arData = new InnerAlgoRunData();

				// get all codes from the portion
				final List<CodeTuple> codes = dao
						.getCodeByPortionKey(codePortionId);

				//
				final ProgressMonitorImpl monitor = new ProgressMonitorImpl(
						algorithmId, codes.size(), codePortionId);

				//
				arData.thread = new Thread() {
					@Override
					public void run() {
						for (CodeTuple code : codes) {
							List<PlagiarismHypoTuple> hypos = algo.process(
									code.codePK, null, monitor);
							try {
								for (PlagiarismHypoTuple hypo : hypos){
									hypo.algorithmFK = algorithmId;
									dao.setPlagiarismHypo(hypo);
								}
							} catch (DataAccessException e) {
								e.printStackTrace();
							}
							monitor.next();
						}
						synchronized (AlgoRunManager.this) {
							algoRunData.remove(new DoubleInt(codePortionId,
									algorithmId));
						}
						System.out.println("thread end");
					}
				};
				arData.begin = System.currentTimeMillis();
				arData.apprEnd = (long) (300000 * (Math.random() + 1));
				algoRunData.put(new DoubleInt(codePortionId, algorithmId),
						arData);
				arData.thread.start();
			} catch (DataAccessException | ClassNotFoundException
					| NoSuchMethodException | SecurityException
					| InstantiationException | IllegalAccessException
					| IllegalArgumentException | InvocationTargetException e) {
				e.printStackTrace();
				throw new RuntimeException(e);
			}
		}
	}

	/**
	 * Method returns the double value from [0;1] segment = (done work) /
	 * (planned work).
	 * 
	 * @param algorithmId
	 * @param codePortionId
	 * @return
	 */
	public double getFinishedPart(int algorithmId, int codePortionId) {
		InnerAlgoRunData ard = algoRunData.get(new DoubleInt(codePortionId,
				algorithmId));

		if (ard == null) {
			throw new IllegalArgumentException();
		} else {
			return ard.state;
		}
	}

	/**
	 * Method returns the double value from [0;1] segment = (done work) /
	 * (planned work). Counts weighted mean of all work ratios of the the given
	 * portion
	 * 
	 * @param codePortionId
	 * @return
	 */
	public double getFinishedPart(int codePortionId) {
		return 0.4;
	}

	/**
	 * 
	 * @param algoId
	 * @return
	 */
	public synchronized String getAlgoName(int algoId)
			throws DataAccessException {
		try {
			return algoLoaf.getAlgorithm(algoId).getName();
		} catch (DataAccessException | ClassNotFoundException
				| NoSuchMethodException | SecurityException
				| InstantiationException | IllegalAccessException
				| IllegalArgumentException | InvocationTargetException e) {
			throw new DataAccessException(e);
		}
	}

	/**
	 * 
	 * @param newProgress
	 * @param algorithmId
	 * @param portionId
	 */
	private synchronized void setProgress(double newProgress, int algorithmId,
			int portionId) {

		DoubleInt di = new DoubleInt(portionId, algorithmId);
		InnerAlgoRunData ard = algoRunData.get(di);
		ard.state = newProgress;
	}

	/**
	 * 
	 * @author doctorpro
	 * 
	 */
	private class ProgressMonitorImpl implements ProgressMonitor {

		public static final double STEP = 0.001;

		private int portionId;
		private int algoId;
		private int portionSize;
		private int codesChecked = 0;
		private double currentCodeProgress;

		private double oldOverallProgress = 0;

		/**
		 * 
		 * @param algoId
		 */
		public ProgressMonitorImpl(int algoId, int portionSize, int portionId) {
			this.algoId = algoId;
			this.portionSize = portionSize;
			this.portionId = portionId;

			prepareNext();
		}

		/**
		 * 
		 */
		public void next() {
			codesChecked++;
			prepareNext();
			notifyAlgoRunManager();
		}

		/**
		 * 
		 */
		@Override
		public void progressMade(double progress) {
			if ((progress < -0.0001) || (progress > 1.0001)) {
				throw new IllegalArgumentException();
			} else {
				currentCodeProgress = progress;
			}
			notifyAlgoRunManager();
		}

		/**
		 * TODO: inspire this method to not only use the [0;1] discretisation,
		 * but also the discretise the time
		 */
		private void notifyAlgoRunManager() {
			// filter here some lesser changes in the state
			double d = overallProgress() - oldOverallProgress;

			// we never like regress
			if (d < 0) {
				throw new IllegalStateException();
			}

			if (d > STEP) {
				oldOverallProgress += d;

				// do the notification
				AlgoRunManager.this.setProgress(oldOverallProgress, algoId,
						portionId);
			}
		}

		/**
		 * 
		 */
		private void prepareNext() {
			currentCodeProgress = 0;
		}

		private double overallProgress() {
			return (currentCodeProgress + codesChecked) / portionSize;
		}
	}

	/**
	 * 
	 * @author doctorpro
	 * 
	 */
	private class InnerAlgoRunData extends AlgoRunData {
		public Thread thread;
	}
}

/**
 * 
 * @author doctorpro
 * 
 */
class DoubleInt {

	int cpId;
	int algoId;

	/**
	 * 
	 * @param cpId
	 * @param algoId
	 */
	public DoubleInt(int cpId, int algoId) {
		this.cpId = cpId;
		this.algoId = algoId;
	}

	/**
	 * 
	 * @param i
	 * @return
	 */
	@Override
	public boolean equals(Object i) {
		DoubleInt di = (DoubleInt) i;
		return this.cpId == di.cpId && this.algoId == di.algoId;
	}

	/**
	 * 
	 */
	@Override
	public int hashCode() {
		return (cpId * 31) ^ algoId;
	}

	@Override
	public String toString() {
		return cpId + ";" + algoId;
	}
}
